/**
 * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com).
 *
 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.gnu.org/licenses/lgpl-3.0.txt
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package io.jpress.utils;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.jfinal.kit.PathKit;
import com.jfinal.log.Log;

public class ClassUtils {

    private static final Log log = Log.getLog(ClassUtils.class);

    public static <T> List<Class<T>> scanSubClass(Class<T> pclazz) {
        return scanSubClass(pclazz, false);
    }

    public static <T> List<Class<T>> scanSubClass(Class<T> pclazz, boolean mustbeCanNewInstance) {
        if (pclazz == null) {
            log.error("scanClass: parent clazz is null");
            return null;
        }

        List<File> classFileList = new ArrayList<File>();
        scanClass(classFileList, PathKit.getRootClassPath());

        List<Class<T>> classList = new ArrayList<Class<T>>();
        for (File file : classFileList) {

            int start = PathKit.getRootClassPath().length();
            int end = file.toString().length() - 6; // 6 == ".class".length();

            String classFile = file.toString().substring(start + 1, end);
            Class<T> clazz = classForName(classFile.replace(File.separator, "."));

            if (clazz != null && pclazz.isAssignableFrom(clazz)) {
                if (mustbeCanNewInstance) {
                    if (clazz.isInterface())
                        continue;

                    if (Modifier.isAbstract(clazz.getModifiers()))
                        continue;
                }
                classList.add(clazz);
            }
        }

        List<File> jarFiles = allJar();

        for (File f : jarFiles) {
            classList.addAll(scanSubClass(pclazz, f, mustbeCanNewInstance));
        }

        return classList;
    }

    private static List<File> allJar() {
        Set<String> jars = findJars(ClassUtils.class.getClassLoader());
        List<File> ret = new ArrayList<>(jars.size());
        for (String fileName : jars) {
            ret.add(new File(fileName));
        }
        return ret;
    }

    public static Set<String> findJars(ClassLoader classLoader) {
        Set<String> ret = new HashSet<>();
        if (classLoader instanceof URLClassLoader) {
            URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
            URL[] urLs = urlClassLoader.getURLs();
            for (URL url : urLs) {
                String s = url.toString();
                if (s.startsWith("file:") && s.endsWith(".jar") && s.contains("jpress")) {// URL对象是抽象的,可能不是本地文件,可能是一个目录,这里就不讨论如何处理了
                    ret.add(url.getPath());
                }
            }
        }
        ClassLoader parent = classLoader.getParent();
        if (parent != null) {
            ret.addAll(findJars(parent));
        }
        return ret;
    }

    public static <T> List<Class<T>> scanSubClass(Class<T> pclazz, File f, boolean mustbeCanNewInstance) {
        List<Class<T>> ret = new ArrayList<>();
        if (pclazz == null) {
            log.error("scanClass: parent clazz is null");
            return ret;
        }

        JarFile jarFile = null;

        try {
            jarFile = new JarFile(f);
            Enumeration<JarEntry> entries = jarFile.entries();

            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String entryName = jarEntry.getName();
                if (!jarEntry.isDirectory() && entryName.endsWith(".class")) {
                    String className = entryName.replace("/", ".").substring(0, entryName.length() - 6);
                    Class<T> clazz = classForName(className);
                    if (clazz != null && pclazz.isAssignableFrom(clazz)) {
                        if (mustbeCanNewInstance) {
                            if (clazz.isInterface())
                                continue;

                            if (Modifier.isAbstract(clazz.getModifiers()))
                                continue;
                        }
                        ret.add(clazz);
                    }
                }
            }

            return ret;

        } catch (IOException e1) {
        } finally {
            if (jarFile != null)
                try {
                    jarFile.close();
                } catch (IOException e) {
                }
        }

        return ret;

    }

    private static Set<String> cannotLoadClassNames = new HashSet<>();

    @SuppressWarnings("unchecked")
    private static <T> Class<T> classForName(String className) {
        if (cannotLoadClassNames.contains(className)) {
            return null;
        }
        Class<T> clazz = null;
        try {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            clazz = (Class<T>) Class.forName(className, false, cl);
        } catch (Throwable e) {
            cannotLoadClassNames.add(className);
            // 取消日志打印,因为失败的东西不少
            // log.error("classForName is error，className:" + className);
        }
        return clazz;
    }

    private static void scanClass(List<File> fileList, String path) {
        File files[] = new File(path).listFiles();
        if (null == files || files.length == 0)
            return;
        for (File file : files) {
            if (file.isDirectory()) {
                scanClass(fileList, file.getAbsolutePath());
            } else if (file.getName().endsWith(".class")) {
                fileList.add(file);
            }
        }
    }

}
